Explaining OOP, Data Structures, and Exception Handling in Interviews
Course: Python Training
Topic: Explaining OOP, Data Structures, and Exception Handling in Interviews
Course Link: Python Training Course
Demo Link: Register for a Course Demo
Website: JustAcademy
1. Introduction
Object-Oriented Programming (OOP), data structures, and exception handling are three important areas of Python technical interviews. Interviewers use questions from these topics to evaluate a candidate's understanding of programming fundamentals, problem-solving skills, code organization, and ability to handle unexpected situations.
Knowing definitions is useful, but a strong interview answer should also include a simple explanation, a practical example, and an explanation of when the concept is useful. Candidates should be able to write small code examples, explain their output, discuss common mistakes, and apply these concepts to real-world problems.
This guide covers essential concepts, Python examples, common interview questions, coding exercises, and strategies for explaining answers clearly.
2. Learning Objectives
- Explain the fundamental principles of OOP in Python.
- Understand classes, objects, inheritance, encapsulation, abstraction, and polymorphism.
- Compare Python lists, tuples, sets, dictionaries, stacks, queues, and other data structures.
- Choose an appropriate data structure for a given problem.
- Explain exceptions, error handling, and custom exceptions.
- Write readable Python code using try, except, else, finally, and raise.
- Answer common technical interview questions confidently.
- Practice coding problems that combine OOP, data structures, and exception handling.
3. How to Answer Technical Interview Questions
A structured answer is easier for an interviewer to follow. Use the following approach when explaining a programming concept.
- Define the concept: Give a clear, accurate definition.
- Explain its purpose: Describe the problem it solves.
- Provide an example: Use a short Python code sample or a familiar real-world scenario.
- Explain the result: Describe what the code does and why.
- Mention trade-offs: Explain limitations, performance, or situations where another approach is better.
Example interview response: “A dictionary is a mutable collection that stores key-value pairs. It is useful when I need to retrieve a value using a unique key. For example, I can store an employee's details using an employee ID as the key. Dictionary lookups are generally O(1) on average, although performance depends on hashing and collisions.”
4. Object-Oriented Programming (OOP)
4.1 What Is OOP?
Object-Oriented Programming is a programming approach that organizes code around objects. An object combines data, represented by attributes, with behavior, represented by methods.
OOP can help organize larger programs, represent real-world entities, reuse functionality, and separate responsibilities into manageable components.
4.2 Class and Object
A class defines the structure and behavior of a particular type of object. An object is an instance of that class.
class Student:
def __init__(self, name, marks):
self.name = name
self.marks = marks
def display_details(self):
print(f"Name: {self.name}")
print(f"Marks: {self.marks}")
student1 = Student("Asha", 88)
student2 = Student("Ravi", 92)
student1.display_details()
student2.display_details()
Explanation:
class Student defines a class named Student.
__init__() initializes the instance when an object is created.
self refers to the current instance.
name and marks are instance attributes.
display_details() is an instance method.
student1 and student2 are separate objects.
4.3 What Is the Difference Between a Class and an Object?
| Class |
Object |
| Defines attributes and methods. |
Is an instance created from a class. |
| Acts as a blueprint. |
Represents a particular instance with its own state. |
| Defined using the class keyword. |
Usually created by calling the class. |
| Example: Student. |
Example: Student("Asha", 88). |
4.4 The Four Main Principles of OOP
The four commonly discussed OOP principles are encapsulation, abstraction, inheritance, and polymorphism.
4.4.1 Encapsulation
Encapsulation combines data and related behavior within a class and provides controlled ways to access or modify an object's state. Python supports conventions and mechanisms for this, but it does not enforce private instance attributes in exactly the same way as some other languages.
class BankAccount:
def __init__(self, balance=0):
if balance < 0:
raise ValueError("Initial balance cannot be negative")
self._balance = balance
def deposit(self, amount):
if amount <= 0:
raise ValueError("Deposit must be positive")
self._balance += amount
def get_balance(self):
return self._balance
account = BankAccount(1000)
account.deposit(500)
print(account.get_balance())
Output:
1500
Explanation: The balance is managed through class methods that validate changes. The leading underscore in _balance indicates that it is intended for internal use. It is a convention, not strict access control.
Interview tip: Explain how encapsulation helps protect an object's state and centralize validation.
4.4.2 Abstraction
Abstraction exposes essential operations while hiding implementation details that users of a component do not need to understand. Python supports abstraction through well-designed interfaces, abstract base classes, and other patterns.
from abc import ABC, abstractmethod
class PaymentMethod(ABC):
@abstractmethod
def pay(self, amount):
pass
class CardPayment(PaymentMethod):
def pay(self, amount):
return f"Card payment processed: {amount}"
payment = CardPayment()
print(payment.pay(500))
Explanation: The abstract base class defines a required pay() operation. A concrete implementation supplies the behavior. The example represents a simplified interface and does not process a real payment.
4.4.3 Inheritance
Inheritance allows a class to derive behavior from another class. The existing class is commonly called the parent or base class, and the derived class is called the child or subclass.
class Employee:
def __init__(self, name):
self.name = name
def introduce(self):
return f"My name is {self.name}"
class Developer(Employee):
def __init__(self, name, language):
super().__init__(name)
self.language = language
def write_code(self):
return f"{self.name} writes {self.language}"
developer = Developer("Neha", "Python")
print(developer.introduce())
print(developer.write_code())
Explanation: Developer inherits the introduce() method from Employee. The super() call reuses the parent class initializer instead of duplicating its logic.
Inheritance is useful when a genuine “is-a” relationship exists. Composition, in which one object contains or uses another object, may be a better choice when components need to vary independently.
4.4.4 Polymorphism
Polymorphism means that different objects can respond to the same operation in their own way. In Python, this commonly appears through method overriding and duck typing.
class Dog:
def speak(self):
return "Woof"
class Cat:
def speak(self):
return "Meow"
def make_sound(animal):
print(animal.speak())
make_sound(Dog())
make_sound(Cat())
Output:
Woof
Meow
Explanation: The function make_sound() works with both objects because each provides a compatible speak() method. It does not need to know the concrete class in advance.
4.5 Types of Methods in Python
| Method Type |
Decorator |
Purpose |
| Instance method |
None required |
Works with an instance and commonly receives self. |
| Class method |
@classmethod |
Works with the class and receives cls. |
| Static method |
@staticmethod |
Groups a related utility operation with a class without automatically receiving self or cls. |
class Temperature:
unit = "Celsius"
def __init__(self, value):
self.value = value
def display(self):
return f"{self.value} {self.unit}"
@classmethod
def get_unit(cls):
return cls.unit
@staticmethod
def is_valid(value):
return isinstance(value, (int, float))
temperature = Temperature(25)
print(temperature.display())
print(Temperature.get_unit())
print(Temperature.is_valid(25))
4.6 Instance Variables and Class Variables
Instance variables belong to individual objects. Class variables are defined on the class and can be shared by its instances unless overridden through instance attributes.
class Employee:
company = "Example Ltd"
def __init__(self, name):
self.name = name
first = Employee("Asha")
second = Employee("Ravi")
print(first.name)
print(second.name)
print(first.company)
Use instance variables for object-specific data such as a person's name. Use class variables for data intended to be shared, such as a common category or default setting. Avoid mutable class variables for data that should be independent for each instance.
4.7 When Should You Use OOP?
- When an application contains related entities with state and behavior.
- When multiple components need clear responsibilities.
- When different implementations should follow a common interface.
- When the project benefits from reusable, testable components.
- When the domain naturally maps to objects such as orders, accounts, products, or users.
OOP is not mandatory for every task. A short data-transformation script may be clearer when written using simple functions rather than multiple classes.
5. Data Structures in Python
5.1 What Is a Data Structure?
A data structure is a way of organizing and storing data so that it can be accessed and modified efficiently. Selecting an appropriate data structure affects program readability, memory usage, and performance.
Python provides built-in data structures such as lists, tuples, sets, and dictionaries. More specialized structures, such as stacks, queues, linked lists, and trees, can be implemented using these building blocks or external libraries.
5.2 List
A list is an ordered, mutable collection that permits duplicate values. It is useful when items need to be stored in sequence and updated.
fruits = ["apple", "banana", "mango"]
fruits.append("orange")
fruits[1] = "grapes"
print(fruits)
print(fruits[0])
Common operations:
append() adds an item to the end.
insert() adds an item at a specified position.
remove() removes the first matching value.
pop() removes and returns an item.
sort() sorts the list in place.
- Slicing selects a range of items.
5.3 Tuple
A tuple is an ordered collection that cannot be structurally modified after creation. It is useful for fixed collections of values, such as coordinates or a function returning several related values.
coordinates = (10, 20)
print(coordinates[0])
print(coordinates[1])
A tuple can contain a mutable object, so immutability of the tuple does not automatically make every object inside it immutable.
5.4 Set
A set stores unique hashable elements and is useful for removing duplicates, membership checks, and set operations. Sets do not provide positional indexing.
numbers = [1, 2, 2, 3, 4, 4]
unique_numbers = set(numbers)
print(unique_numbers)
print(3 in unique_numbers)
Important operations: union (|), intersection (&), difference (-), and symmetric difference (^).
5.5 Dictionary
A dictionary stores key-value pairs. Keys must be hashable and unique within the dictionary. Dictionaries are useful for lookups, configuration, structured records, and counting items.
employee = {
"id": 101,
"name": "Asha",
"department": "IT"
}
print(employee["name"])
print(employee.get("salary", 0))
employee["salary"] = 45000
Use get() when a missing key should return a default value instead of raising a KeyError. Use square-bracket access when the key is required and a missing key should be treated as an error.
5.6 List vs. Tuple vs. Set vs. Dictionary
| Structure |
Ordered |
Mutable |
Duplicates |
Typical Use |
| List |
Yes |
Yes |
Allowed |
Sequences that change over time. |
| Tuple |
Yes |
No, structurally |
Allowed |
Fixed collections of values. |
| Set |
No positional order guarantee |
Yes |
Not among elements |
Uniqueness and membership checks. |
| Dictionary |
Preserves insertion order in modern Python |
Yes |
Keys are unique |
Mapping keys to values. |
5.7 Stack
A stack follows the Last In, First Out (LIFO) principle. The last item added is the first one removed. Stacks are used in undo systems, expression evaluation, and backtracking algorithms.
stack = []
stack.append("first")
stack.append("second")
stack.append("third")
print(stack.pop())
print(stack.pop())
Output:
third
second
Python lists support efficient append and pop operations at the end, making them suitable for many stack implementations.
5.8 Queue
A queue follows the First In, First Out (FIFO) principle. The first item added is the first item removed. Queues are useful for task processing, job scheduling, and message handling.
from collections import deque
queue = deque()
queue.append("task1")
queue.append("task2")
queue.append("task3")
print(queue.popleft())
print(queue.popleft())
Output:
task1
task2
A deque supports efficient additions and removals at both ends. Repeatedly removing the first element from a list using pop(0) is generally less efficient for queue behavior.
5.9 Linked List
A linked list consists of nodes, where each node stores a value and a reference to another node. A singly linked list connects each node to the next node.
- Nodes do not need to be stored next to each other in memory.
- Traversal usually takes O(n) time.
- Insertion or deletion can be efficient when the relevant node reference is already available.
- Accessing an item by position is not typically O(1), unlike list indexing.
Python's built-in list and collections.deque are often more convenient than a custom linked list. An interviewer may still ask about linked lists to evaluate algorithmic understanding.
5.10 Tree and Graph Basics
A tree represents hierarchical relationships using nodes and edges. Common examples include file-system hierarchies and organizational structures.
A graph represents relationships between vertices connected by edges. Graphs may be directed or undirected and may have weights. They are used to represent road networks, social connections, and dependencies.
Common traversal algorithms include:
- Depth-First Search (DFS): Explores one path deeply before backtracking.
- Breadth-First Search (BFS): Explores nodes level by level, commonly using a queue.
6. Time Complexity and Big O Notation
Big O notation describes how the time or space required by an algorithm grows as the input size increases. It is a useful way to compare algorithms and data structure operations.
| Complexity |
Name |
Example |
| O(1) |
Constant |
Accessing a list element by index. |
| O(log n) |
Logarithmic |
Binary search on sorted data. |
| O(n) |
Linear |
Scanning every element in a list. |
| O(n log n) |
Linearithmic |
Typical efficient comparison sorting, such as merge sort. |
| O(n²) |
Quadratic |
Comparing every pair in a straightforward nested loop. |
These are common growth rates, not guarantees for every operation. Actual performance depends on the algorithm, input, and implementation.
6.1 Example: Finding a Duplicate
def has_duplicate_slow(items):
for i in range(len(items)):
for j in range(i + 1, len(items)):
if items[i] == items[j]:
return True
return False
def has_duplicate_fast(items):
seen = set()
for item in items:
if item in seen:
return True
seen.add(item)
return False
print(has_duplicate_slow([1, 2, 3, 2]))
print(has_duplicate_fast([1, 2, 3, 2]))
Explanation: The nested-loop version may perform O(n²) comparisons. The set-based version is O(n) on average for hashable values, using additional memory to store seen items.
7. Selecting the Right Data Structure
| Requirement |
Suitable Structure |
Reason |
| Keep items in sequence and update them |
List |
Supports indexing, iteration, and modification. |
| Store a fixed group of values |
Tuple |
Expresses a collection that should not be structurally changed. |
| Remove duplicate values |
Set |
Stores unique hashable elements. |
| Look up data by identifier |
Dictionary |
Provides efficient average-case key lookup. |
| Process items in LIFO order |
List as stack |
Append and pop from the end. |
| Process items in FIFO order |
Deque |
Efficiently removes items from the left. |
| Represent hierarchical data |
Tree structure |
Models parent-child relationships. |
| Represent networks and connections |
Graph |
Models vertices and relationships between them. |
8. Exception Handling in Python
8.1 What Is an Exception?
An exception is an event that interrupts the normal flow of a program. Python raises exceptions for conditions such as invalid type operations, missing dictionary keys, unavailable files, and division by zero.
Exception handling lets a program respond to anticipated errors in a controlled way. It can help display useful messages, clean up resources, retry safe operations, or stop execution when continuing would be unsafe.
8.2 Syntax of try and except
try:
number = int(input("Enter an integer: "))
result = 100 / number
print(result)
except ValueError:
print("Please enter a valid integer.")
except ZeroDivisionError:
print("The number must not be zero.")
Explanation:
try contains code that may raise an exception.
except handles a matching exception type.
- Different exception types can be handled separately.
- If an exception is not handled by the current block, it normally propagates to a caller or terminates the program if nothing handles it.
8.3 Common Python Exceptions
| Exception |
Typical Cause |
Example |
| ValueError |
A value has the wrong format or is unsuitable for an operation. |
int("hello") |
| TypeError |
An operation is applied to an incompatible type. |
"5" + 5 |
| ZeroDivisionError |
Division by zero. |
10 / 0 |
| IndexError |
A sequence index is outside its valid range. |
[1, 2][5] |
| KeyError |
A dictionary key does not exist. |
{"a": 1}["b"] |
| FileNotFoundError |
A requested file cannot be found. |
Opening a missing file for reading. |
| PermissionError |
An operation is not permitted by the operating system. |
Reading a protected file. |
| AttributeError |
An object does not provide the requested attribute. |
Calling a nonexistent method. |
| ImportError |
An import cannot be completed. |
Importing a name that is unavailable. |
8.4 Using else and finally
The optional else block runs when the try block finishes without an exception. The finally block runs during normal completion or exception propagation and is commonly used for cleanup.
try:
number = int("25")
except ValueError:
print("Invalid number")
else:
print("Conversion successful:", number)
finally:
print("Operation finished")
Output:
Conversion successful: 25
Operation finished
For resource management, prefer a context manager such as with open(...) whenever one is available. It usually handles cleanup more clearly than manually closing resources in a finally block.
8.5 Raising Exceptions
The raise statement allows code to report invalid states or inputs. It is useful when a function cannot safely continue.
def calculate_discount(price, percentage):
if price < 0:
raise ValueError("Price cannot be negative")
if not 0 <= percentage <= 100:
raise ValueError("Percentage must be between 0 and 100")
return price * (1 - percentage / 100)
print(calculate_discount(1000, 10))
Output:
900.0
This function rejects invalid inputs rather than returning a misleading result.
8.6 Custom Exceptions
Custom exceptions allow an application to represent domain-specific errors with meaningful names. They are especially useful when callers need to distinguish one business error from another.
class InsufficientFundsError(Exception):
"""Raised when an account cannot cover a withdrawal."""
class Account:
def __init__(self, balance):
self.balance = balance
def withdraw(self, amount):
if amount <= 0:
raise ValueError("Withdrawal must be positive")
if amount > self.balance:
raise InsufficientFundsError(
"Insufficient balance for withdrawal"
)
self.balance -= amount
return self.balance
account = Account(1000)
try:
account.withdraw(1500)
except InsufficientFundsError as error:
print(error)
Explanation: The custom exception represents a specific business rule violation. A caller can catch it separately from unrelated programming errors.
8.7 Re-raising Exceptions
Sometimes a function needs to log an error or perform a small action before allowing the original exception to propagate. Use raise without an argument inside an exception handler to re-raise the active exception.
def process_record(record):
try:
return int(record["quantity"])
except (KeyError, ValueError):
print("Record does not contain a valid quantity")
raise
In production systems, the logging module is generally preferable to print statements for diagnostic messages.
8.8 Avoid Broad Exception Handling
Using except Exception: without a clear reason can hide programming mistakes and make debugging difficult. Catch the narrowest exception types that the code can reasonably handle.
Less suitable example:
try:
result = 10 / 0
except Exception:
print("Something went wrong")
Better example:
try:
result = 10 / 0
except ZeroDivisionError:
print("Cannot divide by zero")
Do not silently ignore exceptions. If an error cannot be handled safely, allow it to propagate or convert it into a meaningful application-level exception while preserving its cause.
9. Combining OOP, Data Structures, and Exception Handling
Real applications commonly use all three concepts together. A class may manage application state, a dictionary may store objects by ID, and exception handling may manage invalid requests.
9.1 Example: Simple Inventory Manager
class ProductNotFoundError(Exception):
"""Raised when a product ID is not present."""
class Inventory:
def __init__(self):
self.products = {}
def add_product(self, product_id, name, quantity):
if not product_id:
raise ValueError("Product ID is required")
if quantity < 0:
raise ValueError("Quantity cannot be negative")
self.products[product_id] = {
"name": name,
"quantity": quantity
}
def get_product(self, product_id):
try:
return self.products[product_id]
except KeyError as error:
raise ProductNotFoundError(
f"Product {product_id} was not found"
) from error
inventory = Inventory()
inventory.add_product("P101", "Keyboard", 10)
try:
product = inventory.get_product("P101")
print(product)
except ProductNotFoundError as error:
print(error)
9.2 Concepts Demonstrated
- OOP: The Inventory class groups related data and operations.
- Data structure: A dictionary stores product records using product IDs as keys.
- Exception handling: Invalid input raises ValueError, and a missing product raises a custom exception.
- Maintainability: The inventory logic is separated from the code that uses it.
10. Common Interview Questions and Answers: OOP
Q1. What is Object-Oriented Programming?
Answer: OOP is a programming paradigm that organizes code around objects containing data and behavior. It can improve modularity, reuse, and maintainability when used appropriately.
Q2. What is the difference between a class and an object?
Answer: A class defines attributes and methods, while an object is an instance of that class with its own state.
Q3. What is encapsulation?
Answer: Encapsulation groups data and behavior and provides controlled ways to interact with an object's state. In Python, leading underscores communicate intended internal use, and properties or methods can enforce validation.
Q4. What is abstraction?
Answer: Abstraction exposes the operations a user needs while hiding unnecessary implementation details. Abstract base classes and well-designed interfaces are common ways to express it in Python.
Q5. What is inheritance?
Answer: Inheritance allows a class to reuse or specialize behavior from a parent class. It is most useful when the child class represents a meaningful subtype of the parent.
Q6. What is polymorphism?
Answer: Polymorphism allows different objects to respond to the same operation in different ways. Python supports this through overriding and duck typing.
Q7. What is the purpose of self?
Answer: In an instance method, self refers to the instance on which the method is called. It allows the method to access that instance's attributes and other methods.
Q8. What does super() do?
Answer: super() provides access to methods according to Python's method resolution order. It is commonly used to extend parent-class behavior without directly naming the parent implementation.
Q9. What is method overriding?
Answer: Method overriding occurs when a subclass provides its own implementation of a method inherited from a parent class.
Q10. What is the difference between inheritance and composition?
Answer: Inheritance models an “is-a” relationship, while composition models a “has-a” or “uses-a” relationship. Composition can make systems more flexible when components need to be replaced independently.
11. Common Interview Questions and Answers: Data Structures
Q1. What is the difference between a list and a tuple?
Answer: A list is mutable, while a tuple cannot be structurally modified after creation. Lists suit changing collections, whereas tuples are useful for fixed groups of related values.
Q2. What is the difference between a set and a dictionary?
Answer: A set stores unique hashable elements, while a dictionary stores key-value pairs with unique hashable keys.
Q3. Why use a dictionary instead of a list?
Answer: Use a dictionary when data needs to be retrieved by a meaningful key, such as an employee ID. A list is more suitable when order and positional access are central to the task.
Q4. What is a stack?
Answer: A stack follows Last In, First Out. In Python, a list with append() and pop() is often sufficient for stack operations.
Q5. What is a queue?
Answer: A queue follows First In, First Out. Python's collections.deque supports efficient insertion at the right and removal from the left.
Q6. What is Big O notation?
Answer: Big O notation describes how an algorithm's time or space requirements grow with input size. It helps compare scalability and performance characteristics.
Q7. How do you remove duplicates from a list?
Answer: If order is not important and the items are hashable, use set(). If original order must be preserved, use dict.fromkeys() for hashable values or a loop that tracks seen values.
numbers = [3, 1, 3, 2, 1]
unique_numbers = list(dict.fromkeys(numbers))
print(unique_numbers)
Output:
[3, 1, 2]
Q8. What is the difference between a linear search and a binary search?
Answer: Linear search checks elements one by one and takes O(n) time in the worst case. Binary search repeatedly halves the search interval and takes O(log n) time, but it requires sorted data or another structure that supports the necessary ordered access.
Q9. When would you use a set?
Answer: A set is useful for removing duplicates, checking membership, and performing mathematical set operations. Its elements must be hashable.
Q10. Why is deque useful for queues?
Answer: A deque supports efficient removals from the left. A list's pop(0) shifts remaining elements and is generally less efficient for repeatedly processing the front of a queue.
12. Common Interview Questions and Answers: Exception Handling
Q1. What is exception handling?
Answer: Exception handling allows a program to respond to runtime errors in a controlled way using constructs such as try and except.
Q2. What is the difference between a syntax error and an exception?
Answer: A syntax error occurs when Python cannot parse code correctly. An exception occurs when code being executed encounters an error condition, such as division by zero or accessing a missing key.
Q3. What is the difference between else and finally?
Answer: The else block runs when the try block completes without an exception. The finally block runs as control leaves the try statement, including when an exception is propagating.
Q4. What is the purpose of raise?
Answer: The raise statement explicitly raises an exception. It can report invalid inputs, enforce business rules, or re-raise an exception that was caught.
Q5. Can we handle multiple exceptions?
Answer: Yes. You can use separate except blocks for different exception types or handle a tuple of related types in one block. Separate blocks are helpful when each error requires a different response.
Q6. What is a custom exception?
Answer: A custom exception is an application-specific exception class, typically derived from Exception. It provides a meaningful error type for a particular business rule or application condition.
Q7. Why should we avoid bare except?
Answer: A bare except catches BaseException subclasses, including exceptions such as KeyboardInterrupt and SystemExit. This can prevent normal interruption or shutdown. Catch specific exceptions or Exception only when that broad behavior is intentional.
Q8. How do you preserve the original exception when raising another one?
Answer: Use exception chaining with raise NewError(...) from error. This records the original exception as the cause and helps debugging.
Q9. Does finally always run?
Answer: It normally runs when control leaves the try statement, including during exception propagation. It is not an absolute guarantee in cases such as abrupt process termination or certain interpreter shutdown conditions.
Q10. How should an application handle an unexpected error?
Answer: Log useful diagnostic information without exposing secrets, preserve enough context for debugging, clean up resources, and stop or recover safely. Do not silently hide unexpected errors.
13. Practical Coding Interview Questions
Problem 1: Count the Frequency of Each Element
Question: Given a list of values, count how many times each value appears.
from collections import Counter
items = ["apple", "banana", "apple", "orange", "banana", "apple"]
frequency = Counter(items)
print(frequency)
Explanation: Counter is a dictionary-like utility that counts hashable elements. This approach takes O(n) time on average for n input elements.
Problem 2: Find the First Non-Repeating Character
Question: Return the first character that occurs exactly once in a string.
from collections import Counter
def first_non_repeating(text):
counts = Counter(text)
for character in text:
if counts[character] == 1:
return character
return None
print(first_non_repeating("swiss"))
print(first_non_repeating("aabb"))
Output:
w
None
Explanation: The first pass counts each character, and the second pass preserves the original string order while finding the first count of one. Average time complexity is O(n), with O(k) extra space for k distinct characters.
Problem 3: Validate a Positive Number
Question: Write a function that accepts a positive numeric value and raises an exception for invalid values.
def validate_positive(value):
if not isinstance(value, (int, float)) or isinstance(value, bool):
raise TypeError("Value must be numeric")
if value <= 0:
raise ValueError("Value must be positive")
return value
print(validate_positive(12))
Explanation: TypeError communicates an invalid type, while ValueError communicates an unsuitable numeric value. In a production application, additional checks may be needed for non-finite floating-point values.
Problem 4: Create a Class That Stores Records
Question: Create a class that stores records in a dictionary and provides a method to retrieve a record by ID.
class RecordNotFoundError(Exception):
pass
class RecordStore:
def __init__(self):
self.records = {}
def add(self, record_id, value):
self.records[record_id] = value
def get(self, record_id):
if record_id not in self.records:
raise RecordNotFoundError(
f"Record {record_id} does not exist"
)
return self.records[record_id]
store = RecordStore()
store.add(1, {"name": "Asha"})
try:
print(store.get(1))
print(store.get(2))
except RecordNotFoundError as error:
print(error)
Explanation: The class encapsulates record operations, the dictionary provides key-based storage, and the custom exception represents a missing record.
14. Common Mistakes During Interviews
- Memorizing definitions without being able to provide examples.
- Confusing a class with an object.
- Claiming Python enforces all encapsulation rules like a language with strict private fields.
- Using inheritance when composition would produce a simpler design.
- Choosing a list for every problem without considering lookup requirements.
- Claiming dictionary or set operations are always O(1) in every circumstance.
- Forgetting that set elements and dictionary keys must be hashable.
- Catching every exception without understanding which errors may occur.
- Using exceptions to replace ordinary conditional logic unnecessarily.
- Failing to explain the time and space complexity of a solution.
- Ignoring edge cases such as empty input, duplicate values, or missing keys.
- Writing code without checking the expected output.
15. Best Practices for Interview-Ready Python Code
- Use descriptive names for classes, functions, and variables.
- Keep functions focused on one responsibility.
- Use classes when they make related state and behavior easier to manage.
- Select data structures based on required operations and constraints.
- Validate input at appropriate boundaries.
- Catch specific exceptions that can be handled meaningfully.
- Use context managers to manage files and other supported resources.
- Write small tests for normal cases, invalid input, and edge cases.
- Explain time complexity and additional memory usage.
- State assumptions when the interview problem is ambiguous.
- Prefer clear, correct code before attempting unnecessary optimization.
16. Practice Exercises
- Create a Student class with attributes for name and marks, and a method that calculates a grade.
- Build a BankAccount class with deposit and withdrawal methods. Validate amounts and handle insufficient funds.
- Write a function that removes duplicate values while preserving their original order.
- Use a dictionary to count word frequencies in a paragraph.
- Implement a stack with push, pop, and peek operations.
- Use collections.deque to process a queue of support tickets.
- Write a function that reads an integer from a string and handles invalid input.
- Create a custom exception for a missing product in an inventory application.
- Write a function that finds the first non-repeating character in a string.
- Explain the time complexity of each solution and discuss possible alternatives.
17. Mock Interview Preparation Checklist
- Can you explain OOP in your own words?
- Can you create a class, instantiate an object, and define instance methods?
- Can you explain encapsulation, abstraction, inheritance, and polymorphism with examples?
- Can you compare lists, tuples, sets, and dictionaries?
- Can you explain stacks, queues, trees, and graphs at a basic level?
- Can you estimate the time complexity of a simple algorithm?
- Can you explain the purpose of try, except, else, finally, and raise?
- Can you create and handle a custom exception?
- Can you combine a class, a data structure, and exception handling in a small program?
- Can you explain edge cases and test your solution?
- Can you discuss trade-offs rather than simply naming a preferred approach?
18. Conclusion
OOP, data structures, and exception handling are fundamental Python topics that appear frequently in technical interviews. OOP helps organize code into classes and objects, data structures help store and retrieve information effectively, and exception handling makes programs more robust when errors occur.
To prepare effectively, practice explaining concepts in your own words, write short programs, test normal and exceptional cases, and learn to justify your design choices. The strongest answers connect definitions to practical examples and explain why a particular solution is suitable for the problem.
19. Learn More with JustAcademy
Continue developing your Python programming skills through the following JustAcademy resources: